<!doctype html>

<html lang="zh-CN">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width,initial-scale=1" />
  <title>个人自制小工具</title>
  <style>
    :root{
      --bg: #0f172a;
      --card: #0b1220;
      --muted: #94a3b8;
      --accent: #06b6d4;
      --glass: rgba(255,255,255,0.03);
      --radius: 12px;
      --gap: 12px;
      color-scheme: dark;
    }
    *{box-sizing:border-box}
    html,body{height:100%;margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,'Helvetica Neue',Arial}
    body{background:linear-gradient(180deg,var(--bg),#071028);padding:24px;color:#e6eef6}.app{max-width:1100px;margin:0 auto}
header{display:flex;align-items:center;gap:16px;margin-bottom:18px}
h1{font-size:20px;margin:0}
.controls{margin-left:auto;display:flex;gap:8px;align-items:center}

.search{padding:8px 12px;border-radius:10px;background:var(--glass);border:1px solid rgba(255,255,255,0.03);color:inherit}
.btn{background:var(--accent);color:#042027;border:none;padding:8px 10px;border-radius:10px;cursor:pointer}
.btn.ghost{background:transparent;border:1px solid rgba(255,255,255,0.06);color:var(--muted)}

.topbar{display:flex;gap:12px;align-items:center}

.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:var(--gap)}
.card{background:linear-gradient(180deg,rgba(255,255,255,0.02), rgba(255,255,255,0.01));padding:14px;border-radius:var(--radius);min-height:94px;display:flex;flex-direction:column;justify-content:space-between;box-shadow:0 6px 18px rgba(2,6,23,0.5);border:1px solid rgba(255,255,255,0.03)}
.card.dragging{opacity:0.6;transform:scale(0.98)}
.card h3{margin:0 0 8px 0;font-size:16px}
.card p{margin:0;color:var(--muted);font-size:13px}
.card .meta{display:flex;justify-content:space-between;align-items:center;margin-top:10px}
.card .meta .open{background:transparent;border:1px solid rgba(255,255,255,0.06);padding:6px 8px;border-radius:8px;color:var(--muted);cursor:pointer}

.small{font-size:12px;color:var(--muted)}

/* modal */
.modal-backdrop{position:fixed;inset:0;background:rgba(2,6,23,0.6);display:none;align-items:center;justify-content:center;padding:20px}
.modal{background:linear-gradient(180deg,#071126,#061025);padding:18px;border-radius:12px;width:100%;max-width:720px;border:1px solid rgba(255,255,255,0.04)}
label{display:block;margin-top:8px;font-size:13px}
input[type=text],input[type=url],textarea,select{width:100%;padding:8px;border-radius:8px;border:1px solid rgba(255,255,255,0.04);background:transparent;color:inherit}
textarea{min-height:80px}

footer{margin-top:18px;color:var(--muted);font-size:13px;display:flex;gap:12px;align-items:center}
.hint{background:rgba(255,255,255,0.02);padding:8px;border-radius:10px}

@media (max-width:520px){header{flex-direction:column;align-items:flex-start} .controls{margin-left:0}}

  </style>
</head>
<body>
  <div class="app">
    <header>
      <img src="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='36' height='36' viewBox='0 0 24 24'><rect rx='4' width='24' height='24' fill='%2306b6d4'/></svg>" alt="logo" style="width:36px;height:36px;border-radius:8px"/>
      <div>
        <h1>一站式导航</h1>
       
      </div><div class="controls">
    <input id="search" class="search" placeholder="搜索卡片或标签..." />
    <button id="addBtn" class="btn">新增卡片</button>
    <button id="exportBtn" class="btn ghost">导出JSON</button>
    <button id="importBtn" class="btn ghost">导入JSON</button>
  </div>
</header>

<main>
  <div class="topbar">
    
  </div>

  <section id="grid" class="grid" aria-live="polite"></section>

  <footer>
      
  </footer>
</main>

  </div>  <!-- Modal -->  <div id="modalBackdrop" class="modal-backdrop">
    <div class="modal">
      <h3 id="modalTitle">新增卡片</h3>
      <form id="cardForm">
        <input type="hidden" id="cardId" />
        <label>标题</label>
        <input id="title" type="text" required />
        <label>链接 URL</label>
        <input id="url" type="url" placeholder="https://..." required />
        <label>描述（可选）</label>
        <textarea id="desc"></textarea>
        <label>图标 URL（可选）</label>
        <input id="icon" type="url" placeholder="https://.../favicon.png" />
        <label>标签/分类（逗号分隔）</label>
        <input id="tags" type="text" placeholder="例如：工具,hr,研发" /><div style="display:flex;justify-content:flex-end;gap:8px;margin-top:12px">
      <button type="button" id="cancelBtn" class="btn ghost">取消</button>
      <button type="submit" class="btn">保存</button>
    </div>
  </form>
</div>

  </div>  <input id="fileInput" type="file" accept="application/json" style="display:none" />  <script>
  /* ----------------------------- 应用配置 ----------------------------- */
  const STORAGE_KEY = 'intranet_nav_cards_v1';

  /* ----------------------------- 状态 ----------------------------- */
  let cards = []; // {id, title, url, desc, icon, tags:[], created}
  const grid = document.getElementById('grid');
  const modalBackdrop = document.getElementById('modalBackdrop');
  const cardForm = document.getElementById('cardForm');
  const fileInput = document.getElementById('fileInput');

  /* ----------------------------- 启动 ----------------------------- */
  init();

  function init(){
    bindUI();
    loadFromStorage();
    render();
    // 支持其他标签页/窗口修改时的自动更新
    window.addEventListener('storage', (e)=>{ if(e.key===STORAGE_KEY) { loadFromStorage(); render(); }});
    console.log('本网站由谭玮开发设计');
    // 检查是否有旧数据，如果没有则添加默认卡片
    if(cards.length === 0){
      cards = [
        {id:genId(), title:'卡片生成器', url:'卡片生成器.html', desc:'生成各种类型的卡片', icon:'', tags:['工具','生成器'], created:Date.now()},
        {id:genId(), title:'势能转换迭代打卡系统', url:'势能转换迭代打卡系统.html', desc:'用于跟踪和管理势能转换迭代进度', icon:'', tags:['系统','打卡'], created:Date.now()},
      ];
      saveToStorage();
    }
    
  }

  function bindUI(){
    document.getElementById('addBtn').addEventListener('click', ()=>openModal());
    document.getElementById('cancelBtn').addEventListener('click', closeModal);
    cardForm.addEventListener('submit', onSaveCard);
    document.getElementById('search').addEventListener('input', render);
    document.getElementById('exportBtn').addEventListener('click', exportJSON);
    document.getElementById('importBtn').addEventListener('click', ()=>fileInput.click());
    fileInput.addEventListener('change', onFileImport);

    // 拖拽事件委托
    grid.addEventListener('dragstart', (e)=>{
      const id = e.target.dataset.id; if(!id) return; e.dataTransfer.setData('text/id', id); e.target.classList.add('dragging');
    });
    grid.addEventListener('dragend', (e)=>{ e.target.classList.remove('dragging'); });
    grid.addEventListener('dragover', (e)=>{ e.preventDefault(); const after = getDragAfterElement(e.clientY); const dragging = document.querySelector('.dragging'); if(!dragging) return; if(!after) grid.appendChild(dragging); else grid.insertBefore(dragging, after); });
    grid.addEventListener('drop', (e)=>{
      e.preventDefault(); // 更新顺序
      const ordering = Array.from(grid.children).map(n => n.dataset.id);
      reorderByIds(ordering);
      saveToStorage();
      render();
    });

    // 双击卡片快速编辑
    grid.addEventListener('dblclick', (e)=>{
      const cardEl = e.target.closest('.card'); if(!cardEl) return; const id = cardEl.dataset.id; openModal(id);
    });
  }

  /* ----------------------------- 存储 ----------------------------- */
  function loadFromStorage(){
    try{
      const raw = localStorage.getItem(STORAGE_KEY);
      if(raw){ cards = JSON.parse(raw); }
      else { // 初始化示例
        cards = [
          {id:genId(), title:'卡片生成器', url:'卡片生成器.html', desc:'生成各种类型的卡片', icon:'', tags:['工具','生成器'], created:Date.now()},
          {id:genId(), title:'势能转换迭代打卡系统', url:'势能转换迭代打卡系统.html', desc:'用于跟踪和管理势能转换迭代进度', icon:'', tags:['系统','打卡'], created:Date.now()},
        ];
        saveToStorage();
      }
    }catch(err){ console.error('读取存储失败', err); cards = []; }
  }
  function saveToStorage(){
    localStorage.setItem(STORAGE_KEY, JSON.stringify(cards));
  }

  /* ----------------------------- 渲染 ----------------------------- */
  function render(){
    grid.innerHTML='';
    const q = document.getElementById('search').value.trim().toLowerCase();
    const list = cards.filter(c=>{
      if(!q) return true;
      return (c.title||'').toLowerCase().includes(q) || (c.desc||'').toLowerCase().includes(q) || (c.tags||[]).join(',').toLowerCase().includes(q);
    });

    if(list.length===0){ grid.innerHTML = '<div class="small" style="grid-column:1/-1;padding:20px;background:rgba(255,255,255,0.02);border-radius:10px">未找到匹配项 — 尝试新增卡片或清空搜索。</div>'; return; }

    for(const c of list){
      const el = document.createElement('article');
      el.className = 'card';
      el.draggable = true;
      el.dataset.id = c.id;
      el.innerHTML = `
        <div style="display:flex;gap:10px;align-items:center">
          ${c.icon?`<img src="${escapeHtml(c.icon)}" alt="icon" style="width:36px;height:36px;border-radius:8px;object-fit:cover">`:'<div style="width:36px;height:36px;border-radius:8px;background:linear-gradient(180deg,#062a33,#02242b);display:flex;align-items:center;justify-content:center;color:var(--accent);font-weight:600">'+(c.title?c.title[0].toUpperCase():'?')+'</div>'}
          <div style="flex:1">
            <h3>${escapeHtml(c.title)}</n3>
            <p class="small">${escapeHtml(c.desc||'')}</p>
          </div>
        </div>
        <div class="meta">
          <div class="small">${(c.tags||[]).map(t=>'<span style="padding:4px 8px;border-radius:8px;background:rgba(255,255,255,0.02);margin-right:6px">'+escapeHtml(t)+'</span>').join('')}</div>
          <div style="display:flex;gap:8px;align-items:center">
            <button class="open" data-action="open" data-id="${c.id}">打开</button>
            <button class="open" data-action="edit" data-id="${c.id}">编辑</button>
            <button class="open" data-action="del" data-id="${c.id}">删除</button>
          </div>
        </div>
      `;

      // action delegation
      el.addEventListener('click', (e)=>{
        const actionBtn = e.target.closest('[data-action]');
        if(!actionBtn) return;
        const act = actionBtn.dataset.action; const id = actionBtn.dataset.id;
        if(act==='open'){ const card = cards.find(x=>x.id===id); if(card) window.open(card.url,'_blank'); }
        if(act==='edit') openModal(id);
        if(act==='del') { if(confirm('确定要删除该卡片？')) { cards = cards.filter(x=>x.id!==id); saveToStorage(); render(); }}
      });

      grid.appendChild(el);
    }
  }

  /* ----------------------------- 表单 / Modal ----------------------------- */
  function openModal(id){
    resetForm();
    if(id){
      const c = cards.find(x=>x.id===id); if(!c) return; document.getElementById('modalTitle').textContent='编辑卡片'; document.getElementById('cardId').value=c.id; document.getElementById('title').value=c.title; document.getElementById('url').value=c.url; document.getElementById('desc').value=c.desc || ''; document.getElementById('icon').value=c.icon || ''; document.getElementById('tags').value=(c.tags||[]).join(',');
    } else { document.getElementById('modalTitle').textContent='新增卡片'; }
    modalBackdrop.style.display='flex';
    document.getElementById('title').focus();
  }
  function closeModal(){ modalBackdrop.style.display='none'; }
  function resetForm(){ cardForm.reset(); document.getElementById('cardId').value=''; }

  function onSaveCard(e){
    e.preventDefault();
    const id = document.getElementById('cardId').value || genId();
    const title = document.getElementById('title').value.trim();
    const url = document.getElementById('url').value.trim();
    const desc = document.getElementById('desc').value.trim();
    const icon = document.getElementById('icon').value.trim();
    const tags = document.getElementById('tags').value.split(',').map(s=>s.trim()).filter(Boolean);
    const exists = cards.find(x=>x.id===id);
    if(exists){ exists.title=title; exists.url=url; exists.desc=desc; exists.icon=icon; exists.tags=tags; }
    else { cards.push({id,title,url,desc,icon,tags,created:Date.now()}); }
    saveToStorage(); closeModal(); render();
  }

  /* ----------------------------- 导入导出 ----------------------------- */
  function exportJSON(){
    const blob = new Blob([JSON.stringify(cards,null,2)],{type:'application/json'});
    const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'nav_cards.json'; a.click(); URL.revokeObjectURL(a.href);
  }

  function onFileImport(e){
    const f = e.target.files && e.target.files[0]; if(!f) return; const reader = new FileReader(); reader.onload = ()=>{
      try{
        const parsed = JSON.parse(reader.result);
        if(!Array.isArray(parsed)) throw new Error('文件格式不对');
        // 简单验证每项
        const cleaned = parsed.map(p=>({id:p.id||genId(), title:p.title||'Untitled', url:p.url||'#', desc:p.desc||'', icon:p.icon||'', tags:Array.isArray(p.tags)?p.tags:(p.tags?String(p.tags).split(',').map(s=>s.trim()).filter(Boolean):[]), created:p.created||Date.now()}));
        cards = cleaned; saveToStorage(); render(); alert('导入成功');
      }catch(err){ alert('导入失败：'+err.message); }
    };
    reader.readAsText(f);
    e.target.value='';
  }

  /* ----------------------------- 工具函数 ----------------------------- */
  function genId(){ return 'c_'+Math.random().toString(36).slice(2,9); }
  function escapeHtml(s){ if(!s) return ''; return s.replace(/[&<>\"']/g, function(c){ return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":"&#39;"}[c]; }); }

  function reorderByIds(ids){
    const map = Object.fromEntries(cards.map(c=>[c.id,c]));
    const newList = ids.map(id=>map[id]).filter(Boolean);
    // 保留任何未列出的项（以防）
    const remaining = cards.filter(c=>!ids.includes(c.id));
    cards = [...newList, ...remaining];
  }

  // helper: 根据 y 坐标寻找要插入到哪个元素之前
  function getDragAfterElement(y){
    const draggableElements = [...grid.querySelectorAll('.card:not(.dragging)')];
    return draggableElements.reduce((closest, child) => {
      const box = child.getBoundingClientRect();
      const offset = y - box.top - box.height / 2;
      if(offset < 0 && offset > closest.offset){ return {offset: offset, element: child}; } else return closest;
    }, {offset: Number.NEGATIVE_INFINITY}).element;
  }

  </script></body>
</html>